package afaas.framework.util;

import lombok.Getter;
import lombok.Setter;

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class GenericsUtil {

    private static Map<CacheKey, Class<?>> cache1 = new ConcurrentHashMap<CacheKey, Class<?>>();

    private static Map<CacheKey, Class<?>> cache2 = new ConcurrentHashMap<CacheKey, Class<?>>();

    public static Class<?> findGenericClassWithoutCache(Class<?> instantiatedClazz, Class<?> xAxis, int yAxis) {

        CheckUtil.checkArgument(instantiatedClazz != null);
        CheckUtil.checkArgument(xAxis != null);
        CheckUtil.checkArgument(yAxis >= 0);
        CheckUtil.checkArgument(!instantiatedClazz.equals(xAxis));

        List<Class> extendsAndImplementsChain = findOneExtendsAndImplementsChain(instantiatedClazz, xAxis);

        TypeVariable<?>[] instantiatedClazzTypeParameters = instantiatedClazz.getTypeParameters();
        for(Type instantiatedClazzTypeParameter : instantiatedClazzTypeParameters) {
            CheckUtil.checkArgument(
                    (instantiatedClazzTypeParameter instanceof Class)
                            ||
                            (instantiatedClazzTypeParameter instanceof ParameterizedType)
            );
        }

        Map<Class<?>, List<Class<?>>> actualTypes4Class = new LinkedHashMap<>();
        Map<String, Class> typeVariablesBuffer = new LinkedHashMap<>();

        for(int i=extendsAndImplementsChain.size() -1 ; i>0; i--) {
            //for(Class<?> tClass = instantiatedClazz; !tClass.equals(xAxis); tClass = tClass.getSuperclass()) {
            Class<?> tClass = extendsAndImplementsChain.get(i);
            Class<?> superClassOrInterface = extendsAndImplementsChain.get(i-1);

            Type type = null;
            if(superClassOrInterface.isInterface()) {
                Type[] types = tClass.getGenericInterfaces();
                for(Type tType : types) {
                    Class interfaceClz = null;
                    if(tType instanceof Class) {
                        interfaceClz = ((Class<?>) tType);
                    }else if(tType instanceof ParameterizedType){
                        interfaceClz = (Class) ((ParameterizedType) tType).getRawType();
                    }else {
                        throw new RuntimeException();
                    }
                    if(superClassOrInterface.equals(interfaceClz)) {
                        type = tType;
                        break;
                    }
                }
            }else {
                type = tClass.getGenericSuperclass();
            }

            if(!(type instanceof ParameterizedType)) {
                actualTypes4Class.put(superClassOrInterface, new ArrayList<>());
                continue;
            }

            ParameterizedType parameterizedType = (ParameterizedType) type;
            Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
            TypeVariable<?>[] typeParameters = superClassOrInterface.getTypeParameters();

            CheckUtil.checkArgument(actualTypeArguments.length == typeParameters.length);

            List<Class<?>> actualTypes = new ArrayList<>();
            for(Type actualTypeArgument : actualTypeArguments) {
                if(actualTypeArgument instanceof Class) {
                    actualTypes.add((Class<?>) actualTypeArgument);
                }else if(actualTypeArgument instanceof ParameterizedType){
                    Type actualTypeArgumentType = ((ParameterizedType) actualTypeArgument).getRawType();
                    if(actualTypeArgumentType instanceof Class) {
                        actualTypes.add((Class<?>) actualTypeArgumentType);
                    }else {
                        throw new RuntimeException("Can not find rawclass of ParameterizedType, type is " + actualTypeArgument.getTypeName());
                    }
                }else if(actualTypeArgument instanceof TypeVariable){
                    Class<?> typeVariableCalss = typeVariablesBuffer.get(((TypeVariable)actualTypeArgument).getName());
                    CheckUtil.checkNotNull(typeVariableCalss);
                    actualTypes.add(typeVariableCalss);
                }else {
                    throw new RuntimeException("Can not recognize actualTypeArgument , type is " + actualTypeArgument);
                }
            }

            for(int j=0; j< typeParameters.length; j++) {
                TypeVariable typeParameter = typeParameters[j];
                if(typeParameter instanceof TypeVariable) {
                    typeVariablesBuffer.put(typeParameter.getName(), actualTypes.get(j));
                }
            }

            actualTypes4Class.put(superClassOrInterface, actualTypes);

        }

        if(yAxis < actualTypes4Class.get(xAxis).size()) {
            return actualTypes4Class.get(xAxis).get(yAxis);
        }else {
            return null;
        }
    }

    @SuppressWarnings("unchecked")
    public static List<Class> findOneExtendsAndImplementsChain(Class currentClz, Class targetClz){

        if(!targetClz.isAssignableFrom(currentClz)) {
            return null;
        }
        else if(currentClz.equals(targetClz)) {
            ArrayList<Class> classes = new ArrayList<>();
            classes.add(currentClz);
            return classes;
        }
        else if(currentClz.isInterface()) {
            return findOneInterfaceExtendsChain(currentClz, targetClz);
        }else {
            Class superClass = currentClz.getSuperclass();
            List<Class> extendsAndImplementsChain = findOneExtendsAndImplementsChain(superClass, targetClz);
            if(extendsAndImplementsChain != null) {
                extendsAndImplementsChain.add(currentClz);
                return extendsAndImplementsChain;
            }

            return findOneInterfaceExtendsChain(currentClz, targetClz);
        }
    }

    public static List<Class> findOneInterfaceExtendsChain(Class currentClz, Class targetInterface){

        CheckUtil.checkArgument(targetInterface.isInterface());
        if(currentClz.equals(targetInterface)) {
            List<Class> clz = new ArrayList<>();
            clz.add(currentClz);
            return clz;
        }

        Class[] superInterfaces = currentClz.getInterfaces();
        if(superInterfaces == null || superInterfaces.length == 0) {
            return null;
        }

        for(int i=0; i<superInterfaces.length; i++) {
            Class superInterface = superInterfaces[i];
            List<Class> extendsInterfacesChain = findOneInterfaceExtendsChain(superInterface, targetInterface);
            if(extendsInterfacesChain != null) {
                extendsInterfacesChain.add(currentClz);
                return extendsInterfacesChain;
            }
        }

        return null;
    }

    public static Class<?> findGenericClass(Class<?> instantiatedClazz, Class<?> xAxis, int yAxis) {

        Class<?> ret = null;

        if((ret = cache2.get(new CacheKey(instantiatedClazz, yAxis, xAxis))) != null) {
            return ret;
        }else {
            ret = findGenericClassWithoutCache(instantiatedClazz, xAxis, yAxis);
            if(ret != null) {
                cache2.put(new CacheKey(instantiatedClazz, yAxis, xAxis), ret);
                return ret;
            }else {
                return null;
            }
        }
    }

    /**
     * @param index, 0 based
     */
    public static Class<?> findGenericClass(Class<?> clz, int index, Class<?> root) {

        CheckUtil.checkNotNull(clz);
        CheckUtil.checkNotNull(root);
        CheckUtil.checkArgument(index >= 0);

        Class<?> ret = null;

        if((ret = cache1.get(new CacheKey(clz, index, root))) != null) {
            return ret;
        }else {
            ret = findGenericClassWithoutCache(clz, index, root);
            if(ret != null) {
                cache1.put(new CacheKey(clz, index, root), ret);
                return ret;
            }else {
                return null;
            }
        }
    }

    public static Class<?> findGenericClassWithoutCache(Class<?> clz, int index, Class<?> root) {

        CheckUtil.checkNotNull(clz);
        CheckUtil.checkNotNull(root);
        CheckUtil.checkArgument(index >= 0);

        Class<?> ret = null;

        ret = findCurrentClass(clz, index, root);
        if(ret != null) {
            return ret;
        }

        return findInterfaces(clz, index, root);

    }

    protected static Class<?> findCurrentClass(Class<?> clz, int index, Class<?> root){

        Class<?> superClass = clz.getSuperclass();
        if(superClass == null) {
            return null;
        }

        Type genericSuperClass = clz.getGenericSuperclass();

        Class<?> actualTypeArgument = findActualTypeArgument(genericSuperClass, index, root);

        if(actualTypeArgument instanceof Class) {
            return (Class<?>) actualTypeArgument;
        }else {
            return findCurrentClass(clz.getSuperclass(), index, root);
        }

    }

    protected static Class<?> findInterfaces(Class<?> clz, int index, Class<?> root){

        Type[] genericInterfaces = clz.getGenericInterfaces();
        if(genericInterfaces == null || genericInterfaces.length <= 0) {
            return null;
        }

        for(Type genericInterface : genericInterfaces) {

            Class<?> actualTypeArgument = findActualTypeArgument(genericInterface, index, root);

            if(actualTypeArgument instanceof Class) {
                return (Class<?>) actualTypeArgument;
            }else {
                continue;
            }

        }

        Class<?>[] interfaces = clz.getInterfaces();
        for(Class<?> interfaceClz : interfaces) {

            Class<?> interfaceRet = findInterfaces(interfaceClz, index, root);
            if(interfaceRet != null) {
                return interfaceRet;
            }else {
                continue;
            }
        }


        return null;

    }


    private static Class<?> findActualTypeArgument(Type type, int index, Class<?> root){

        if(!(type instanceof ParameterizedType)) {
            return null;
        }

        ParameterizedType parameterizedType = (ParameterizedType) type;

        Type rawType = parameterizedType.getRawType();
        Class<?> clz = null;
        if(rawType instanceof Class) {
            clz = (Class<?>) rawType;
        }else {
            return null;
        }

        if(!root.isAssignableFrom(clz)) {
            return null;
        }

        Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
        if(actualTypeArguments.length <= index) {
            return null;
        }

        Type actualTypeArgument = actualTypeArguments[index];
        if(actualTypeArgument instanceof Class) {
            return (Class<?>) actualTypeArgument;
        }else if(actualTypeArgument instanceof ParameterizedType){
            Type actualTypeArgumentType = ((ParameterizedType) actualTypeArgument).getRawType();
            if(actualTypeArgumentType instanceof Class) {
                return (Class<?>) actualTypeArgumentType;
            }else {
                throw new RuntimeException("Can not find rawclass of ParameterizedType, type is " + actualTypeArgument.getTypeName());
            }
        }else {
            return null;
        }

    }





    public static class CacheKey{

        @Getter @Setter private Class<?> clz;

        @Getter @Setter private Integer index;

        @Getter @Setter private Class<?> root;

        public CacheKey(Class<?> clz, int index, Class<?> root) {
            this.clz = clz;
            this.index = index;
            this.root = root;
        }

        public int hashCode() {

            int prime = 17;

            int total = clz.hashCode();

            total = prime * total + index.hashCode();
            total = prime *total + root.hashCode();

            return total;
        }

        public boolean equals(Object obj) {

            if(!(obj instanceof CacheKey)) {
                return false;
            }

            if(this == obj) {
                return true;
            }

            CacheKey out = (CacheKey) obj;

            return clz.equals(out.getClz())
                    && index.equals(out.getIndex())
                    && root.equals(out.getRoot());

        }

    }

}
